package be.kdg.bombermanunlimited.server.network;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.List;

import be.kdg.bombermanunlimited.client.network.listeners.IRingServerListener;
import be.kdg.bombermanunlimited.server.network.interfaces.INetworkManager;
import be.kdg.bombermanunlimited.server.network.interfaces.INetworkManagerListener;
import be.kdg.bombermanunlimited.server.remote.finalclasses.Game;

public class NetworkManager extends UnicastRemoteObject implements
		INetworkManager {

	private static final long serialVersionUID = -8865783509300950888L;
	private static final int testPort = 59753;

	// listeners that get updated when nodes are removed
	private List<INetworkManagerListener> listeners;

	private Node startNode;
	private Game game;

	/**
	 * Create a new network manager
	 * 
	 * @param csf
	 *            A socket factory for the client
	 * @param ssf
	 *            A socket factory for the server
	 * @throws RemoteException
	 */
	public NetworkManager(Game game, RMIClientSocketFactory csf,
			RMIServerSocketFactory ssf) throws RemoteException {
		super(0, csf, ssf);

		startNode = null;
		listeners = new ArrayList<INetworkManagerListener>();
		this.game = game;
	}

	public void addNetworkManagerListener(INetworkManagerListener listener) {
		listeners.add(listener);
	}

	public void removeNetworkManagerListener(INetworkManagerListener listener) {
		listeners.remove(listener);
	}

	/**
	 * Een functie waar eerste controle wordt gedaan of een client wel mag
	 * connecten. Deze functie wordt rechtstreeks door de client aangeroepen via
	 * rmi.
	 */
	@Override
	public void registerNode(String ip, int port, IRingServerListener listener)
			throws RemoteException {
		System.out.println("[NetworkManager] Registering new node: " + ip + ":"
				+ port);

		// check of server vol zit
		if (game.isFull()) {
			System.out.println("[NetworkManager] Server full");
			listener.serverFull();
			return;
		}

		// Create a new node
		Node node = new Node(ip, port, listener);
		registerNode(node);
	}

	private void registerNode(Node node) {
		// registreren via een aparte thread om timeouts te voorkomen!
		new Thread(new AddNode(node)).start();
	}

	private void sendNewConnectionMessage(Node src, Node start) {
		// use a thread to create the connection message, TCP-timeout!!
		Thread th = new Thread(new SendConnectMessage(src, start));
		th.start();
	}

	/**
	 * Controlleer of je met een Node n kan connecten. Nodig om op firewall/NAT
	 * etc te checken.
	 * 
	 * @param n
	 * @return
	 */
	private boolean canConnect(Node n) {
		try {
			Socket sckt = new Socket(n.getIp(), n.getPort());
			sckt.shutdownInput();
			sckt.shutdownOutput();
			sckt.close();

			// it can connect :)
			return true;
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		// can't connect :(
		return false;
	}

	/**
	 * Een client meldt dat hij niet met een andere client kan verbinden.
	 */
	@Override
	public void cantConnect(String srcNode, int srcPort, String dstNode,
			int dstPort) throws RemoteException {
		System.out.println("[NetworkManager] Can't connection to " + dstNode
				+ ":" + dstPort);

		// search the source and destination node
		Node dst = startNode.findNode(dstNode, dstPort);
		Node src = startNode.findNode(srcNode, srcPort);

		// remove the links
		src.removeConnectedNode(dst);
		dst.removeConnectedNode(src);

		// it seems the destination can't accept connections anymore :s
		// check if he is still up
		try {
			dst.getListener().isUp();
			// the node is on-line, but probably behind a firewall/router,
			// set a flag so we know we can't connect with the node
			dst.canAcceptConnections(false);
		} catch (RemoteException ex) {
			System.out.println("[NetworkManager] client offline: "
					+ dst.getIp() + ":" + dst.getPort());
			// the node isn't on-line anymore
			removeNode(dst);
		}

		// send a new connection message
		sendNewConnectionMessage(src, startNode);

		dumpConnections();
	}

	/**
	 * Controlleer een aantal zaken omdat de node onverwachts weg is gevallen
	 * volgens een client.
	 */
	@Override
	public void connectionLost(String sourceNode, int srcPort, String destNode,
			int destPort) throws RemoteException {
		System.out.println("[NetworkManager] Connection lost from "
				+ sourceNode + ":" + srcPort + " to " + destNode + ":"
				+ destPort);

		// get the source node
		Node src = startNode.findNode(sourceNode, srcPort);

		if (src == null) {
			System.out.println("Can't find the source! " + sourceNode + ":"
					+ srcPort);
			return;
		}

		// ignore, it was the server to test the connection
		if (src.findNode(destNode, destPort) == null) {
			// if node is found, then it was a test connection
			System.out
					.println("[NetworkManager] Client controlled off-line or test connection lost --> ignore.");
			return;
		}

		Node dest = src.findNode(destNode, destPort);

		// if n = null then it is already removed from this list
		if (dest != null) {
			try {
				// check of hij toch nog on-line is
				dest.getListener().isUp();
				// zo ja, waarschijnlijk firewall probleem. Markeer hem zodat
				// hij geen connecties meer kan ontvangen.
				dest.canAcceptConnections(false);
			} catch (RemoteException ex) {
				System.out.println("[NetworkManager] client offline: "
						+ destNode + ":" + destPort);
				// anders is de node off-line, verwijder hem van de lijst
				removeNode(dest);
				return;
			}
			dumpConnections();
		} else {
			System.out.println("[NetworkManager] dest not found: " + destNode
					+ ":" + destPort);
		}

		src.removeConnectedNode(dest);
		if (startNode.isInRing(src) && startNode != src) {
			System.out
					.println("[NetworkManager] Connection count == 0 -> sending new connection");
			sendNewConnectionMessage(src, startNode);
		} else {
			System.out.println("[NetworkManager] Source still in ring!");
		}
	}

	@Override
	public void removeNode(String ip, int port, IRingServerListener listener)
			throws RemoteException {
		if (startNode != null) {
			// find the node to remove
			Node removingNode = startNode.findNode(ip, port);

			if (removingNode != null) {
				removeNode(removingNode);
			} else {
				System.out.println("[NetworkManager] Can't remove node: " + ip
						+ ":" + port);
			}
		}
	}

	private void removeNode(Node removingNode) {
		// we will need this variable at various places and gets reinitialized
		List<Node> ring = null;

		// get the childs of this node, because they need probably new
		// connections
		List<Node> childs = removingNode.getConnectedNodes();

		// if startNode == n then we need some special code
		// to be sure that the ring
		if (startNode.equals(removingNode)) {
			// get the complete ring
			ring = startNode.getNodesInRing();
			Node bestStart = null;

			// if size == 1 then the last client has disconnected :(
			if (ring.size() > 1) {
				// select the second in the list, because the first is the
				// startnode
				// itself :)
				bestStart = ring.get(1);

				// now we are trying to find a node which can accept connections
				int index = 2;
				while (index < ring.size()
						&& !ring.get(index).canAcceptConnections()) {
					if (ring.get(index).getConnectionCount() < bestStart
							.getConnectionCount()) {
						bestStart = ring.get(index);
					}

					index++;
				}
			}

			startNode = bestStart;
		}

		if (startNode != null) {
			System.out.println("[NetworkManager] Removed "
					+ removingNode.getIp() + ":" + removingNode.getPort());
			removingNode.removeAllNodes();

			// get all nodes in the ring
			ring = startNode.getNodesInRing();
			// now remove the all nodes in child that are stil in the ring, they
			// don't need a new connection setup
			childs.removeAll(ring);

			// create the new connections for the childs that aren't in the ring
			// anymore
			for (Node child : childs) {
				sendNewConnectionMessage(child, startNode);
			}
		} else {
			System.out
					.println("[NetworkManager] The last client has disconnected");
		}

		dumpConnections();

		for (INetworkManagerListener l : listeners) {
			l.onNodeRemoved(removingNode);
		}
	}

	private void dumpConnections() {
		System.out.println("Start dump");
		System.out.println("----------");
		if (startNode != null) {
			List<Node> values = startNode.getNodesInRing();
			for (Node n : values) {
				dumpNode(n);
			}
		} else {
			System.out.println("No nodes left!");
		}
		System.out.println("----------");
	}

	private void dumpNode(Node n) {
		System.out.println(n.toString());
	}

	private class AddNode implements Runnable {
		private Node nodeToAdd;

		public AddNode(Node nodeToAdd) {
			this.nodeToAdd = nodeToAdd;
		}

		@Override
		public void run() {
			if (startNode == null) {
				// if startNode == null then this node is the new first node
				startNode = nodeToAdd;
				try {
					nodeToAdd.getListener().addSuccess();
				} catch (RemoteException e) {
					e.printStackTrace();
				}
			} else {
				// check if the node is already in the ring, remove if there it
				// is
				// found
				Node tmp = startNode.findNode(nodeToAdd.getIp(),
						nodeToAdd.getPort());
				if (tmp != null) {
					try {
						tmp.getListener().isUp();
						nodeToAdd.getListener().socketAlreadyInUse();
						return;
					} catch (RemoteException e) {
						removeNode(tmp);
					}
				}

				// startnode can get null because of removing tmp if tmp wasn't
				// null
				if (startNode != null) {
					// send a message to a node to connect with this node
					sendNewConnectionMessage(nodeToAdd, startNode);
				} else {
					startNode = nodeToAdd;
				}

				try {
					nodeToAdd.getListener().addSuccess();
				} catch (RemoteException e) {
					e.printStackTrace();
				}
			}

			// log connections
			dumpConnections();
		}
	}

	private class SendConnectMessage implements Runnable {
		private Node nodeToPutInRing;
		private Node start;

		public SendConnectMessage(Node nodeToPutInRing, Node start) {
			this.nodeToPutInRing = nodeToPutInRing;
			this.start = start;
		}

		@Override
		public void run() {
			// check if the new node can accept connections
			nodeToPutInRing.canAcceptConnections(canConnect(nodeToPutInRing));

			// look for a good second node to connect with
			Node secondNode = null;
			for (Node n : start.getNodesInRing()) {
				if (nodeToPutInRing.canAcceptConnections()) {
					// if the new node can accept connections, pick the one with
					// the minimum connections
					if (secondNode == null) {
						secondNode = n;
					} else if (secondNode.getConnectionCount() > n
							.getConnectionCount()) {
						secondNode = n;
					}
				} else {
					// look for a node which can accept connections and with the
					// least connections
					if (secondNode == null && n.canAcceptConnections()) {
						secondNode = n;
					} else if (secondNode.getConnectionCount() > n
							.getConnectionCount() && n.canAcceptConnections()) {
						secondNode = n;
					}
				}
			}

			if (secondNode != null) {
				// look who can accept the connection and send the message
				if (secondNode.canAcceptConnections()) {
					try {
						secondNode.getListener().connectTo(
								nodeToPutInRing.getIp(),
								nodeToPutInRing.getPort());

						// create the links
						secondNode.addConnectedNode(nodeToPutInRing);
					} catch (RemoteException e) {
						e.printStackTrace();
						try {
							secondNode.getListener().isUp();
						} catch (RemoteException e1) {
							removeNode(secondNode);
							registerNode(nodeToPutInRing);
						}

					}
				} else {
					try {
						nodeToPutInRing.getListener().connectTo(
								secondNode.getIp(), secondNode.getPort());

						// create the links
						nodeToPutInRing.addConnectedNode(secondNode);
					} catch (RemoteException e) {
						e.printStackTrace();
						try {
							secondNode.getListener().isUp();
						} catch (RemoteException e1) {
							removeNode(secondNode);
							registerNode(nodeToPutInRing);
						}
					}
				}
			} else {
				try {
					// this message says he can't connect :(
					nodeToPutInRing.getListener().connectTo("", -1);
				} catch (RemoteException e) {
					e.printStackTrace();
				}
			}

			dumpConnections();
		}

	}
}
